// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.Cng;
using Microsoft.AspNetCore.Cryptography.Internal;

namespace Microsoft.AspNetCore.Cryptography.SafeHandles
{
    /// <summary>
    /// Represents a handle to a BCrypt algorithm provider from which keys and hashes can be created.
    /// </summary>
    internal unsafe sealed class BCryptAlgorithmHandle : BCryptHandle
    {
        // Called by P/Invoke when returning SafeHandles
        private BCryptAlgorithmHandle() { }

        /// <summary>
        /// Creates an unkeyed hash handle from this hash algorithm.
        /// </summary>
        public BCryptHashHandle CreateHash()
        {
            return CreateHashCore(null, 0);
        }

        private BCryptHashHandle CreateHashCore(byte* pbKey, uint cbKey)
        {
            BCryptHashHandle retVal;
            int ntstatus = UnsafeNativeMethods.BCryptCreateHash(this, out retVal, IntPtr.Zero, 0, pbKey, cbKey, dwFlags: 0);
            UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
            CryptoUtil.AssertSafeHandleIsValid(retVal);

            retVal.SetAlgorithmProviderHandle(this);
            return retVal;
        }

        /// <summary>
        /// Creates an HMAC hash handle from this hash algorithm.
        /// </summary>
        public BCryptHashHandle CreateHmac(byte* pbKey, uint cbKey)
        {
            Debug.Assert(pbKey != null);
            return CreateHashCore(pbKey, cbKey);
        }

        /// <summary>
        /// Imports a key into a symmetric encryption or KDF algorithm.
        /// </summary>
        public BCryptKeyHandle GenerateSymmetricKey(byte* pbSecret, uint cbSecret)
        {
            BCryptKeyHandle retVal;
            int ntstatus = UnsafeNativeMethods.BCryptGenerateSymmetricKey(this, out retVal, IntPtr.Zero, 0, pbSecret, cbSecret, 0);
            UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
            CryptoUtil.AssertSafeHandleIsValid(retVal);

            retVal.SetAlgorithmProviderHandle(this);
            return retVal;
        }

        /// <summary>
        /// Gets the name of this BCrypt algorithm.
        /// </summary>
        public string GetAlgorithmName()
        {
            // First, calculate how many characters are in the name.
            uint byteLengthOfNameWithTerminatingNull = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, null, 0);
            CryptoUtil.Assert(byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char), "byteLengthOfNameWithTerminatingNull % sizeof(char) == 0 && byteLengthOfNameWithTerminatingNull > sizeof(char)");
            uint numCharsWithoutNull = (byteLengthOfNameWithTerminatingNull - 1) / sizeof(char);

            if (numCharsWithoutNull == 0)
            {
                return String.Empty; // degenerate case
            }

            // Allocate a string object and write directly into it (CLR team approves of this mechanism).
            string retVal = new String((char)0, checked((int)numCharsWithoutNull));
            uint numBytesCopied;
            fixed (char* pRetVal = retVal)
            {
                numBytesCopied = GetProperty(Constants.BCRYPT_ALGORITHM_NAME, pRetVal, byteLengthOfNameWithTerminatingNull);
            }
            CryptoUtil.Assert(numBytesCopied == byteLengthOfNameWithTerminatingNull, "numBytesCopied == byteLengthOfNameWithTerminatingNull");
            return retVal;
        }

        /// <summary>
        /// Gets the cipher block length (in bytes) of this block cipher algorithm.
        /// </summary>
        public uint GetCipherBlockLength()
        {
            uint cipherBlockLength;
            uint numBytesCopied = GetProperty(Constants.BCRYPT_BLOCK_LENGTH, &cipherBlockLength, sizeof(uint));
            CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
            return cipherBlockLength;
        }

        /// <summary>
        /// Gets the hash block length (in bytes) of this hash algorithm.
        /// </summary>
        public uint GetHashBlockLength()
        {
            uint hashBlockLength;
            uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_BLOCK_LENGTH, &hashBlockLength, sizeof(uint));
            CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
            return hashBlockLength;
        }

        /// <summary>
        /// Gets the key lengths (in bits) supported by this algorithm.
        /// </summary>
        public BCRYPT_KEY_LENGTHS_STRUCT GetSupportedKeyLengths()
        {
            BCRYPT_KEY_LENGTHS_STRUCT supportedKeyLengths;
            uint numBytesCopied = GetProperty(Constants.BCRYPT_KEY_LENGTHS, &supportedKeyLengths, (uint)sizeof(BCRYPT_KEY_LENGTHS_STRUCT));
            CryptoUtil.Assert(numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT), "numBytesCopied == sizeof(BCRYPT_KEY_LENGTHS_STRUCT)");
            return supportedKeyLengths;
        }

        /// <summary>
        /// Gets the digest length (in bytes) of this hash algorithm provider.
        /// </summary>
        public uint GetHashDigestLength()
        {
            uint digestLength;
            uint numBytesCopied = GetProperty(Constants.BCRYPT_HASH_LENGTH, &digestLength, sizeof(uint));
            CryptoUtil.Assert(numBytesCopied == sizeof(uint), "numBytesCopied == sizeof(uint)");
            return digestLength;
        }

        public static BCryptAlgorithmHandle OpenAlgorithmHandle(string algorithmId, string implementation = null, bool hmac = false)
        {
            // from bcrypt.h
            const uint BCRYPT_ALG_HANDLE_HMAC_FLAG = 0x00000008;

            // from ntstatus.h
            const int STATUS_NOT_FOUND = unchecked((int)0xC0000225);

            BCryptAlgorithmHandle algHandle;
            int ntstatus = UnsafeNativeMethods.BCryptOpenAlgorithmProvider(out algHandle, algorithmId, implementation, dwFlags: (hmac) ? BCRYPT_ALG_HANDLE_HMAC_FLAG : 0);

            // error checking
            if (ntstatus == STATUS_NOT_FOUND)
            {
                string message = String.Format(CultureInfo.CurrentCulture, Resources.BCryptAlgorithmHandle_ProviderNotFound, algorithmId);
                throw new CryptographicException(message);
            }
            UnsafeNativeMethods.ThrowExceptionForBCryptStatus(ntstatus);
            CryptoUtil.AssertSafeHandleIsValid(algHandle);

            return algHandle;
        }

        // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you.
        protected override bool ReleaseHandle()
        {
            return (UnsafeNativeMethods.BCryptCloseAlgorithmProvider(handle, dwFlags: 0) == 0);
        }

        public void SetChainingMode(string chainingMode)
        {
            fixed (char* pszChainingMode = chainingMode ?? String.Empty)
            {
                SetProperty(Constants.BCRYPT_CHAINING_MODE, pszChainingMode, checked((uint)(chainingMode.Length + 1 /* null terminator */) * sizeof(char)));
            }
        }
    }
}
